/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* $Id: AlignmentContext.java,v 1.1 2007/04/12 06:41:19 cvsuser Exp $ */

package com.wisii.wisedoc.layoutmanager.inline;

import com.wisii.wisedoc.document.Constants;
import com.wisii.wisedoc.document.datatype.Length;
import com.wisii.wisedoc.document.datatype.LengthBase;
import com.wisii.wisedoc.document.datatype.SimplePercentBaseContext;
import com.wisii.wisedoc.fonts.Font;

/**
 * The alignment context is carried within a LayoutContext and as part of the
 * Knuth Inline elements to facilitate proper line building. All measurements
 * are in mpt.
 */
public class AlignmentContext implements Constants, Cloneable
{

	/** The height or BPD of this context */
	private int areaHeight;
	/** The computed line-height property value applicable */
	private int lineHeight;
	/** The distance in BPD from the top of the box to the alignmentPoint */
	private int alignmentPoint;
	/** The baseline shift value in effect */
	private int baselineShiftValue;
	/** The computed alignment baseline identifier */
	private int alignmentBaselineIdentifier;
	/** The x height */
	private int xHeight;
	private ScaledBaselineTable scaledBaselineTable = null;
	private ScaledBaselineTable actualBaselineTable = null;
	private AlignmentContext parentAlignmentContext = null;

	/**
	 * Creates a new instance of AlignmentContext for graphics areas.
	 * 
	 * @param height
	 *            the total height of the area
	 * @param alignmentAdjust
	 *            the alignment-adjust property
	 * @param alignmentBaseline
	 *            the alignment-baseline property
	 * @param baselineShift
	 *            the baseline-shift property
	 * @param dominantBaseline
	 *            the dominant-baseline property
	 * @param parentAlignmentContext
	 *            the parent alignment context
	 */
	public AlignmentContext(int height, Length alignmentAdjust,
			int alignmentBaseline, Length baselineShift, int dominantBaseline,
			AlignmentContext parentAlignmentContext)
	{

		this.areaHeight = height;
		this.lineHeight = height;
		this.xHeight = height;
		this.parentAlignmentContext = parentAlignmentContext;
		this.scaledBaselineTable = parentAlignmentContext
				.getScaledBaselineTable();
		setAlignmentBaselineIdentifier(alignmentBaseline,
				parentAlignmentContext.getDominantBaselineIdentifier());
		setBaselineShift(baselineShift);
		int dominantBaselineIdentifier = parentAlignmentContext
				.getDominantBaselineIdentifier();
		boolean newScaledBaselineTableRequired = false;
		if (baselineShiftValue != 0)
		{
			newScaledBaselineTableRequired = true;
		}
		switch (dominantBaseline)
		{
		case EN_AUTO:
			newScaledBaselineTableRequired = baselineShiftValue != 0;
			break;
		case EN_USE_SCRIPT: // TODO
			break;
		case EN_NO_CHANGE:
			break;
		case EN_RESET_SIZE:
			newScaledBaselineTableRequired = true;
			break;
		default:
			newScaledBaselineTableRequired = true;
			dominantBaselineIdentifier = dominantBaseline;
			break;
		}
		actualBaselineTable = ScaledBaselineTableFactory
				.makeGraphicsScaledBaselineTable(height,
						dominantBaselineIdentifier, scaledBaselineTable
								.getWritingMode());
		if (newScaledBaselineTableRequired)
		{
			scaledBaselineTable = ScaledBaselineTableFactory
					.makeGraphicsScaledBaselineTable(height,
							dominantBaselineIdentifier, scaledBaselineTable
									.getWritingMode());
		}
		setAlignmentAdjust(alignmentAdjust);
	}

	/**
	 * Creates a new instance of AlignmentContext
	 * 
	 * @param font
	 *            the font
	 * @param lineHeight
	 *            the computed value of the lineHeight property
	 * @param alignmentAdjust
	 *            the alignment-adjust property
	 * @param alignmentBaseline
	 *            the alignment-baseline property
	 * @param baselineShift
	 *            the baseline-shift property
	 * @param dominantBaseline
	 *            the dominant-baseline property
	 * @param parentAlignmentContext
	 *            the parent alignment context
	 */
	public AlignmentContext(Font font, int lineHeight, Length alignmentAdjust,
			int alignmentBaseline, Length baselineShift, int dominantBaseline,
			AlignmentContext parentAlignmentContext)
	{
		this.areaHeight = font.getAscender() - font.getDescender();
		this.lineHeight = lineHeight;
		this.parentAlignmentContext = parentAlignmentContext;
		this.scaledBaselineTable = parentAlignmentContext
				.getScaledBaselineTable();
		this.xHeight = font.getXHeight();
		setAlignmentBaselineIdentifier(alignmentBaseline,
				parentAlignmentContext.getDominantBaselineIdentifier());
		setBaselineShift(baselineShift);
		int dominantBaselineIdentifier = parentAlignmentContext
				.getDominantBaselineIdentifier();
		boolean newScaledBaselineTableRequired = false;
		if (baselineShiftValue != 0)
		{
			newScaledBaselineTableRequired = true;
		}
		switch (dominantBaseline)
		{
		case EN_AUTO:
			newScaledBaselineTableRequired = baselineShiftValue != 0;
			break;
		case EN_USE_SCRIPT: // TODO
			break;
		case EN_NO_CHANGE:
			break;
		case EN_RESET_SIZE:
			newScaledBaselineTableRequired = true;
			break;
		default:
			newScaledBaselineTableRequired = true;
			dominantBaselineIdentifier = dominantBaseline;
			break;
		}
		actualBaselineTable = ScaledBaselineTableFactory
				.makeFontScaledBaselineTable(font, dominantBaselineIdentifier,
						scaledBaselineTable.getWritingMode());
		if (newScaledBaselineTableRequired)
		{
			scaledBaselineTable = ScaledBaselineTableFactory
					.makeFontScaledBaselineTable(font,
							dominantBaselineIdentifier, scaledBaselineTable
									.getWritingMode());
		}
		setAlignmentAdjust(alignmentAdjust);
	}

	/**
	 * Creates a new instance of AlignmentContext based simply on the font and
	 * the writing mode.
	 * 
	 * @param font
	 *            the font
	 * @param lineHeight
	 *            the computed value of the lineHeight property
	 * @param writingMode
	 *            the current writing mode
	 */
	public AlignmentContext(Font font, int lineHeight, int writingMode)
	{
		this.areaHeight = font.getAscender() - font.getDescender();
		this.lineHeight = lineHeight;
		this.xHeight = font.getXHeight();
		this.parentAlignmentContext = null;
		this.scaledBaselineTable = ScaledBaselineTableFactory
				.makeFontScaledBaselineTable(font, writingMode);
		this.actualBaselineTable = scaledBaselineTable;
		this.alignmentBaselineIdentifier = getDominantBaselineIdentifier();
		this.alignmentPoint = font.getAscender();
		this.baselineShiftValue = 0;
	}

	/**
	 * Returns the alignment point for this context. This is the point on the
	 * start edge of the area this context applies to measured from the before
	 * edge of the area.
	 * 
	 * @return the default alignment point
	 */
	public int getAlignmentPoint()
	{
		return alignmentPoint;
	}

	/**
	 * Returns the current value of baseline shift in effect.
	 * 
	 * @return the baseline shift
	 */
	public int getBaselineShiftValue()
	{
		return baselineShiftValue;
	}

	/**
	 * Returns the current alignment baseline identifier
	 * 
	 * @return the alignment baseline identifier
	 */
	public int getAlignmentBaselineIdentifier()
	{
		return alignmentBaselineIdentifier;
	}

	/**
	 * Sets the current alignment baseline identifer. For alignment-baseline
	 * values of "auto" and "baseline" this method does the conversion into the
	 * appropriate computed value assuming script is "auto" and the fo is not
	 * fo:character.
	 * 
	 * @param alignmentBaseline
	 *            the alignment-baseline property
	 * @param parentDominantBaselineIdentifier
	 *            the dominant baseline of the parent fo
	 */
	private void setAlignmentBaselineIdentifier(int alignmentBaseline,
			int parentDominantBaselineIdentifier)
	{
		switch (alignmentBaseline)
		{
		case EN_AUTO: // fall through
		case EN_BASELINE:
			this.alignmentBaselineIdentifier = parentDominantBaselineIdentifier;
			break;
		case EN_BEFORE_EDGE:
		case EN_TEXT_BEFORE_EDGE:
		case EN_CENTRAL:
		case EN_MIDDLE:
		case EN_AFTER_EDGE:
		case EN_TEXT_AFTER_EDGE:
		case EN_IDEOGRAPHIC:
		case EN_ALPHABETIC:
		case EN_HANGING:
		case EN_MATHEMATICAL:
			this.alignmentBaselineIdentifier = alignmentBaseline;
			break;
		}
	}

	/**
	 * Sets the current alignment baseline identifer. For alignment-baseline
	 * values of "auto" and "baseline" this method does the conversion into the
	 * appropriate computed value assuming script is "auto" and the fo is not
	 * fo:character.
	 * 
	 * @param alignmentAdjust
	 *            the alignment-adjust property
	 */
	private void setAlignmentAdjust(Length alignmentAdjust)
	{
		int beforeEdge = actualBaselineTable.getBaseline(EN_BEFORE_EDGE);
		switch (alignmentAdjust.getEnum())
		{
		case EN_AUTO:
			alignmentPoint = beforeEdge
					- actualBaselineTable
							.getBaseline(alignmentBaselineIdentifier);
			break;
		case EN_BASELINE:
			alignmentPoint = beforeEdge;
			break;
		case EN_BEFORE_EDGE:
		case EN_TEXT_BEFORE_EDGE:
		case EN_CENTRAL:
		case EN_MIDDLE:
		case EN_AFTER_EDGE:
		case EN_TEXT_AFTER_EDGE:
		case EN_IDEOGRAPHIC:
		case EN_ALPHABETIC:
		case EN_HANGING:
		case EN_MATHEMATICAL:
			alignmentPoint = beforeEdge
					- actualBaselineTable
							.getBaseline(alignmentAdjust.getEnum());
			break;
		default:
			alignmentPoint = beforeEdge
					+ alignmentAdjust.getValue(new SimplePercentBaseContext(
							null, LengthBase.ALIGNMENT_ADJUST, lineHeight));
			break;
		}
	}

	/**
	 * Return the scaled baseline table for this context.
	 * 
	 * @return the scaled baseline table
	 */
	public ScaledBaselineTable getScaledBaselineTable()
	{
		return this.scaledBaselineTable;
	}

	/**
	 * Return the dominant baseline identifier.
	 * 
	 * @return the dominant baseline identifier
	 */
	public int getDominantBaselineIdentifier()
	{
		return scaledBaselineTable.getDominantBaselineIdentifier();
	}

	/**
	 * Return the writing mode.
	 * 
	 * @return the writing mode
	 */
	public int getWritingMode()
	{
		return scaledBaselineTable.getWritingMode();
	}

	/**
	 * Calculates the baseline shift value based on the baseline-shift property
	 * value.
	 * 
	 * @param baselineShift
	 *            the baseline shift property value
	 * @return the computed baseline shift value
	 */
	private void setBaselineShift(Length baselineShift)
	{
		baselineShiftValue = 0;
		ScaledBaselineTable sbt = null;
		switch (baselineShift.getEnum())
		{
		case EN_BASELINE: // Nothing to do
			break;
		case EN_SUB:
			baselineShiftValue = Math.round(-(xHeight / 2)
					+ parentAlignmentContext
							.getActualBaselineOffset(EN_ALPHABETIC));
			break;
		case EN_SUPER:
			baselineShiftValue = Math.round(parentAlignmentContext.getXHeight()
					+ parentAlignmentContext
							.getActualBaselineOffset(EN_ALPHABETIC));
			break;
		case 0: // A <length> or <percentage> value
			baselineShiftValue = baselineShift
					.getValue(new SimplePercentBaseContext(null,
							LengthBase.CUSTOM_BASE, parentAlignmentContext
									.getLineHeight()));
			break;
		}
	}

	/**
	 * Return the parent alignment context.
	 * 
	 * @return the parent alignment context
	 */
	public AlignmentContext getParentAlignmentContext()
	{
		return parentAlignmentContext;
	}

	/**
	 * Return the offset between the current dominant baseline and the parent
	 * dominant baseline.
	 * 
	 * @return the offset in shift direction
	 */
	public int getBaselineOffset()
	{
		if (parentAlignmentContext == null)
		{
			return 0;
		}
		return parentAlignmentContext.getScaledBaselineTable().getBaseline(
				alignmentBaselineIdentifier)
				- scaledBaselineTable.deriveScaledBaselineTable(
						parentAlignmentContext.getDominantBaselineIdentifier())
						.getBaseline(alignmentBaselineIdentifier)
				- scaledBaselineTable.getBaseline(parentAlignmentContext
						.getDominantBaselineIdentifier()) + baselineShiftValue;
	}

	/**
	 * Return the offset between the current dominant baseline and the outermost
	 * parent dominant baseline.
	 * 
	 * @return the offet in shift direction
	 */
	public int getTotalBaselineOffset()
	{
		int offset = 0;
		if (parentAlignmentContext != null)
		{
			offset = getBaselineOffset()
					+ parentAlignmentContext.getTotalBaselineOffset();
		}
		return offset;
	}

	/**
	 * Return the offset between the alignment baseline and the outermost parent
	 * dominant baseline.
	 * 
	 * @return the offset in shift direction
	 */
	public int getTotalAlignmentBaselineOffset()
	{
		return getTotalAlignmentBaselineOffset(alignmentBaselineIdentifier);
	}

	/**
	 * Return the offset between the given alignment baseline and the outermost
	 * parent dominant baseline.
	 * 
	 * @param alignmentBaselineId
	 *            the alignment baseline
	 * @return the offset
	 */
	public int getTotalAlignmentBaselineOffset(int alignmentBaselineId)
	{
		int offset = baselineShiftValue;
		if (parentAlignmentContext != null)
		{
			offset = parentAlignmentContext.getTotalBaselineOffset()
					+ parentAlignmentContext.getScaledBaselineTable()
							.getBaseline(alignmentBaselineId)
					+ baselineShiftValue;
		}
		return offset;
	}

	/**
	 * Return the offset between the dominant baseline and the given actual
	 * baseline
	 * 
	 * @param baselineIdentifier
	 *            the baseline
	 * @return the offset
	 */
	public int getActualBaselineOffset(int baselineIdentifier)
	{
		// This is the offset from the dominant baseline to the alignment
		// baseline
		int offset = getTotalAlignmentBaselineOffset()
				- getTotalBaselineOffset();
		// Add the offset to the actual baseline we want
		offset += actualBaselineTable.deriveScaledBaselineTable(
				alignmentBaselineIdentifier).getBaseline(baselineIdentifier);
		return offset;
	}

	/**
	 * Return the offset the outermost parent dominant baseline and the top of
	 * this box.
	 * 
	 * @return the offset
	 */
	private int getTotalTopOffset()
	{
		int offset = getTotalAlignmentBaselineOffset() + getAltitude();
		return offset;
	}

	/**
	 * Return the total height of the context.
	 * 
	 * @return the height
	 */
	public int getHeight()
	{
		return areaHeight;
	}

	/**
	 * Return the line height of the context.
	 * 
	 * @return the height
	 */
	public int getLineHeight()
	{
		return lineHeight;
	}

	/**
	 * The altitude of the context that is the height above the alignment point.
	 * 
	 * @return the altitude
	 */
	public int getAltitude()
	{
		return alignmentPoint;
	}

	/**
	 * The depth of the context that is the height below alignment point.
	 * 
	 * @return the altitude
	 */
	public int getDepth()
	{
		return getHeight() - alignmentPoint;
	}

	/**
	 * The x height of the context.
	 * 
	 * @return the x height
	 */
	public int getXHeight()
	{
		return this.xHeight;
	}

	/**
	 * Resizes the line as specified. Assumes that the new alignment point is on
	 * the dominant baseline, that is this function should be called for line
	 * areas only.
	 * 
	 * @param newLineHeight
	 *            the new height of the line
	 * @param newAlignmentPoint
	 *            the new alignment point
	 */
	public void resizeLine(int newLineHeight, int newAlignmentPoint)
	{
		areaHeight = newLineHeight;
		alignmentPoint = newAlignmentPoint;
		scaledBaselineTable.setBeforeAndAfterBaselines(alignmentPoint,
				alignmentPoint - areaHeight);
	}

	/**
	 * Returns the offset from the before-edge of the parent to this context.
	 * 
	 * @return the offset for rendering
	 */
	public int getOffset()
	{
		int offset = 0;
		if (parentAlignmentContext != null)
		{
			offset = parentAlignmentContext.getTotalTopOffset()
					- getTotalTopOffset();
		} else
		{
			offset = getAltitude()
					- scaledBaselineTable.getBaseline(EN_TEXT_BEFORE_EDGE);
		}
		return offset;
	}

	/**
	 * Returns an indication if we still use the initial baseline table. The
	 * initial baseline table is the table generated by the Line LM.
	 * 
	 * @return true if this is still the initial baseline table
	 */
	public boolean usesInitialBaselineTable()
	{
		return parentAlignmentContext == null
				|| (scaledBaselineTable == parentAlignmentContext
						.getScaledBaselineTable() && parentAlignmentContext
						.usesInitialBaselineTable());
	}

	private boolean isHorizontalWritingMode()
	{
		return (getWritingMode() == EN_LR_TB || getWritingMode() == EN_RL_TB);
	}

	/** @see java.lang.Object#toString() */
	public String toString()
	{
		StringBuffer sb = new StringBuffer(64);
		sb.append("ah=" + areaHeight);
		sb.append(" lp=" + lineHeight);
		sb.append(" ap=" + alignmentPoint);
		sb.append(" ab=" + alignmentBaselineIdentifier);
		sb.append(" bs=" + baselineShiftValue);
		return sb.toString();
	}

	public boolean equals(Object obj)
	{
		if (!(obj instanceof AlignmentContext))
		{
			return false;
		}
		// /** The height or BPD of this context */
		// private int areaHeight;
		// /** The computed line-height property value applicable */
		// private int lineHeight;
		// /** The distance in BPD from the top of the box to the alignmentPoint
		// */
		// private int alignmentPoint;
		// /** The baseline shift value in effect */
		// private int baselineShiftValue;
		// /** The computed alignment baseline identifier */
		// private int alignmentBaselineIdentifier;
		// /** The x height */
		// private int xHeight;
		// private ScaledBaselineTable scaledBaselineTable = null;
		// private ScaledBaselineTable actualBaselineTable = null;
		// private AlignmentContext parentAlignmentContext = null;
		AlignmentContext ac = (AlignmentContext) obj;
		return areaHeight == ac.areaHeight
				&& lineHeight == ac.lineHeight
				&& alignmentPoint == ac.alignmentPoint
				&& baselineShiftValue == ac.baselineShiftValue
				&& alignmentBaselineIdentifier == ac.alignmentBaselineIdentifier
				&& xHeight == ac.xHeight
				&& (scaledBaselineTable == null ? ac.scaledBaselineTable == null
						: scaledBaselineTable.equals(ac.scaledBaselineTable))
				&& (actualBaselineTable == null ? ac.actualBaselineTable == null
						: actualBaselineTable.equals(ac.actualBaselineTable))
				&& isparentequal(parentAlignmentContext,
						ac.parentAlignmentContext);
	}

	private boolean isparentequal(AlignmentContext pac1, AlignmentContext pac2)
	{
		if (pac1 == null)
		{
			if (pac2 == null)
			{
				return true;
			}
			return false;
		} else
		{
			if (pac2 != null)
			{
				return pac1.areaHeight == pac2.areaHeight
						&& pac1.lineHeight == pac2.lineHeight
						&& pac1.alignmentPoint == pac2.alignmentPoint
						&& pac1.baselineShiftValue == pac2.baselineShiftValue
						&& pac1.alignmentBaselineIdentifier == pac2.alignmentBaselineIdentifier
						&& pac1.xHeight == pac2.xHeight
						&& (pac1.scaledBaselineTable == null ? pac2.scaledBaselineTable == null
								: pac1.scaledBaselineTable
										.equals(pac2.scaledBaselineTable))
						&& (pac1.actualBaselineTable == null ? pac2.actualBaselineTable == null
								: pac1.actualBaselineTable
										.equals(pac2.actualBaselineTable))
						&& isparentequal(pac1.parentAlignmentContext,
								pac2.parentAlignmentContext);
			}
			return false;
		}

	}

	public AlignmentContext clone()
	{
		try
		{
			AlignmentContext ac = (AlignmentContext) super.clone();
			ac.actualBaselineTable = actualBaselineTable.clone();
			ac.scaledBaselineTable = scaledBaselineTable.clone();
			if (parentAlignmentContext != null)
			{
				ac.parentAlignmentContext = parentAlignmentContext.clone();
			}
			return (AlignmentContext) super.clone();
		} catch (CloneNotSupportedException ex)
		{
			// SHOULD NEVER OCCUR - all members are primitive types!
			return null;
		}
	}
}
